package com.google.code.ldaputils ;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

/**
 * Utility class containing static methods for common LDAP operations. 
 * 
 * @author Andrew Lim (aka Dante Shamest)
 */
public class Ldap {
  
  /**
   * Maps Attributes into an object.
   */
  public static abstract class AttributesMapper implements SearchResultMapper {
    public abstract Object map(Attributes attributes) ;
    public Object map(SearchResult searchResult)  {
      return map( searchResult.getAttributes() );
    }
  }

  /**
   * Maps a SearchResult into an object.
   */
  public static interface SearchResultMapper {
    public Object map( SearchResult searchResult ) throws RuntimeException;
  }
  
  /**
   * Closes the context without throwing a NamingException.
   * @param ctx
   */
  public static void closeContextQuietly( DirContext ctx ) {
    try {
      ctx.close();
    } catch ( NamingException ex ) {
      // who gives a shit
    }
  }
  
  /**
   * Convenience method to create Attributes.
   * @param attrObjs an array of arrays of Strings
   * @return
   */
  public static Attributes createAttributes( String[][] attrObjs ) {
    Attributes attrs = new BasicAttributes();
    for ( int x=0; x<attrObjs.length; ++x ) {
      String attrId = attrObjs[ x ][ 0 ].toString() ;
      Attribute attr = new BasicAttribute(attrId ) ;
      for ( int y=1; y<attrObjs[x].length; ++y ) {
        attr.add( attrObjs[x][y].toString() );
      }
      attrs.put( attr );
    }
    return attrs ;
  }  
  
  /**
   * Convenience method to create ModificationItems.
   * @param attrObjs an array of arrays of objects
   * @return
   */
  public static 
  ModificationItem[] createModificationItems( String[][] attrObjs ) {
    ModificationItem[] items = new ModificationItem[ attrObjs.length ];
    for ( int x=0; x<attrObjs.length; ++x ) {
      String attrId = attrObjs[ x ][ 0 ].toString() ;
      Attribute attr = new BasicAttribute( attrId );
      ModificationItem item = new ModificationItem( DirContext.REPLACE_ATTRIBUTE
                                                    ,attr ) ;
      for ( int y=1; y<attrObjs[x].length; ++y ) {
        attr.add( attrObjs[x][y].toString() );
      }
      items[ x ] = item ;
    }
    return items ;
  }

  /**
   * Creates an initial directory context.
   * This method can be used for simple authentication.
   * @param url
   * @param dn
   * @param password
   * @return
   * @throws NamingException
   */
  public static DirContext createContext(String url, String dn, String password) 
  throws NamingException {
    return new InitialDirContext( createEnvironment(url,dn,password) );
  }
  
  /**
   * Creates a Hashtable with environment values.
   * @param url
   * @param dn
   * @param password
   * @return
   */
  public static Hashtable
  createEnvironment(String url, String dn, String password) {
    Hashtable env = new Hashtable<Object,Object>();
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, url );
    env.put(Context.SECURITY_PRINCIPAL, dn );
    env.put(Context.SECURITY_CREDENTIALS, password );
    return env ;
  }

  /**
   * Returns all the attributes associated with the named object as an object.
   * @param ctx
   * @param dn
   * @param mapper
   * @return
   * @throws NamingException
   */
  public static Object lookup( DirContext ctx, String dn, 
                               AttributesMapper mapper ) 
  throws NamingException {
    return mapper.map( ctx.getAttributes(dn) );
  }
  
  /**
   * Get the value of a String attribute. If more than one attribute value 
   * exists for the specified attribute, only the first one will be returned.
   * @param attrs
   * @param attrId
   * @return
   */
  public static String getStringAttribute( Attributes attrs, String attrId ) {
    return getStringAttribute( attrs, attrId, null );
  }
  
  /**
   * Get the value of a String attribute. If more than one attribute value 
   * exists for the specified attribute, only the first one will be returned.
   * @param attrs
   * @param attrId
   * @param def
   * @return
   */
  public static String getStringAttribute( Attributes attrs, String attrId, 
                                           String def ) {
    try {
      Attribute attr = attrs.get(attrId);
      return (attr==null) ? def : attr.get().toString() ;
    } catch ( NamingException ex ) {
      throw new RuntimeException( ex.getMessage(), ex );
    }
  }

  /**
   * Convenience method to put an Attribute with multiple values into
   * Attributes.
   * @param attrs
   * @param attrId
   * @param values
   */
  public static void 
  putMultiValueAttribute( Attributes attrs, String attrId, String[] values ) {
    BasicAttribute attr = new BasicAttribute( attrId );
    setAttributeValues( attr, values );
    attrs.put( attr );
  }

  /**
   * Search for all objects matching the supplied filter.
   * @param ctx
   * @param base
   * @param filter
   * @param controls
   * @param mapper
   * @return
   * @throws NamingException
   */
  public static List search( DirContext ctx, String base, String filter, 
                             SearchControls controls, SearchResultMapper mapper)
  throws NamingException {
    ArrayList objects = new ArrayList();
    NamingEnumeration ne = ctx.search( base, filter, controls );
    while ( ne.hasMoreElements() ) {
      objects.add( mapper.map((SearchResult) ne.nextElement()) );
    }
    ne.close();
    return objects ;
  }
  
  /**
   * Search for all objects matching the supplied filter.
   * @param ctx
   * @param base
   * @param filter
   * @param mapper
   * @return
   * @throws NamingException
   */
  public static List search( DirContext ctx, String base, String filter, 
                             SearchResultMapper mapper )
  throws NamingException {
    SearchControls searchControls = new SearchControls() ;
    searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE );
    return search( ctx, base, filter, searchControls, mapper );
  }  
  
  /**
   * Convenience method to put multiples values into an Attribute.
   * @param attr
   * @param values
   */
  public static void setAttributeValues( Attribute attr, String[] values ) {
    attr.clear();
    for ( int i=0; i<values.length; ++i ) {
      attr.add( values[i] );
    }
  }
}